Skip to content

feat: append LSP diagnostic codes to flymake messages#5036

Merged
jcs090218 merged 4 commits into
emacs-lsp:masterfrom
alberti42:show-diagnostic-codes
May 14, 2026
Merged

feat: append LSP diagnostic codes to flymake messages#5036
jcs090218 merged 4 commits into
emacs-lsp:masterfrom
alberti42:show-diagnostic-codes

Conversation

@alberti42
Copy link
Copy Markdown
Contributor

Currently, lsp-diagnostics--flymake-update-diagnostics extracts :message and :severity? from each LSP diagnostic but silently drops :code?. As a result, flymake displays messages like:

"variable_XYZ" is possibly unbound

without the diagnostic rule name that the language server also provides.

This patch binds :code? and appends it to the message text when present:

"variable_XYZ" is possibly unbound [reportPossiblyUnbound]

This is a small but valuable change. The diagnostic code is the primary handle users need to:

  • search for the error online
  • look it up in the language server's documentation
  • write a precise inline suppression comment (e.g. # pyright: ignore[reportPossiblyUnbound])

Without the code, users must guess the rule name. The fix is a two-line change: bind :code? in the existing -let* destructuring and conditionally append [code] to the message.

The code field is optional in the LSP spec, so the format falls back gracefully to the plain message when absent.

@jcs090218
Copy link
Copy Markdown
Member

Can you do this to flycheck too? Thanks! :D

@alberti42
Copy link
Copy Markdown
Contributor Author

Hi @jcs090218, thanks for the quick reply already.

I checked flycheck. The good news is that we don't need to change anything in the flycheck branch. The code is already correct and exactly displays the error code as [CODE].

You can see that code? was already deconstructed and passed to :id. Flycheck then takes care of rendering it correctly.

(-map (-lambda ((&Diagnostic :message :severity? :tags? :code? :source?
                                    :range (&Range :start (start &as &Position
                                                                 :line      start-line
                                                                 :character start-character)
                                                   :end   (end   &as &Position
                                                                 :line      end-line
                                                                 :character end-character))))
               (flycheck-error-new
                :buffer (current-buffer)
                :checker checker
                :filename buffer-file-name
                :message message
                :level (lsp-diagnostics--flycheck-calculate-level severity? tags?)
                :id code?
                :group source?
                :line (lsp-translate-line (1+ start-line))
                :column (1+ (lsp-translate-column start-character))
                :end-line (lsp-translate-line (1+ end-line))
                :end-column (unless (lsp--position-equal start end)
                              (1+ (lsp-translate-column end-character))))))

What was missing was precisely the flymake branch where the code was not displayed. If this PR makes it to upstream, then both branches display the same message and error code.

@alberti42
Copy link
Copy Markdown
Contributor Author

alberti42 commented Apr 18, 2026

Hi @jcs090218 and maintainers,

I looked into the three failing CI checks (the snapshot, true rows on ubuntu-latest, ubuntu-24.04-arm, and macos-latest). They predate this PR: the same three jobs fail on the latest master runs, and the root cause is an Emacs-snapshot / ert-runner compatibility issue (ert-compat load error, void-function defmacro*) that trips lsp-byte-compilation-test before any of the code in this PR is exercised. From what I understand, the stable matrix (Emacs 28.2 / 29.4 / 30.2 across Linux, macOS, Windows) is all green, and those jobs are marked continue-on-error anyway, so I don't think they should block the merge.

While I have your attention, would you be open to a follow-up that makes this more configurable per client? Two directions where I'd appreciate your feedback:

  1. Composable formatter: a per-client hook, e.g. :diagnostic-message-fn, that takes the LSP diagnostic and returns the flymake message string. Users/clients could then format as they like (include source, code, code-description href, severity, etc.). The current behavior would be the default.
  2. Simple toggle: a per-client boolean (e.g. :flymake-show-diagnostic-code) to suppress the [code] suffix for servers where it's noisy. Cheaper, but less flexible.

My motivation is that for some servers the codes are essential (Pyright, tsc), while for others they're just visual noise on every line. Happy to prepare a demo PR for whichever direction you prefer, either extending this one, or as a separate PR once this lands. Let me know what you think is the best.

@jcs090218
Copy link
Copy Markdown
Member

I checked flycheck. The good news is that we don't need to change anything in the flycheck branch. The code is already correct and exactly displays the error code as [CODE].

Hmm.. I think the text renders differently. These are my sideline screenshots (applied your changes):

flycheck

flycheck

flymake

flymake

@jcs090218
Copy link
Copy Markdown
Member

I looked into the three failing CI checks (the snapshot, true rows on ubuntu-latest, ubuntu-24.04-arm, and macos-latest). They predate this PR: the same three jobs fail on the latest master runs, and the root cause is an Emacs-snapshot / ert-runner compatibility issue (ert-compat load error, void-function defmacro*) that trips lsp-byte-compilation-test before any of the code in this PR is exercised. From what I understand, the stable matrix (Emacs 28.2 / 29.4 / 30.2 across Linux, macOS, Windows) is all green, and those jobs are marked continue-on-error anyway, so I don't think they should block the merge.

Yeah, there are breaking changes on the Emacs snapshot. No need to worry about those job failures (at least for now). :)

While I have your attention, would you be open to a follow-up that makes this more configurable per client? Two directions where I'd appreciate your feedback:

I prefer the Composable formatter approach because it's more flexible and gives users greater control.

Thanks for bringing this up! :D

@alberti42 alberti42 force-pushed the show-diagnostic-codes branch 2 times, most recently from d468183 to 723a064 Compare May 3, 2026 14:47
Add `lsp-diagnostics-flymake-message-formatter', a defcustom that decides
how an LSP `Diagnostic' plist is rendered into the single string flymake
stores per diagnostic.  The default
`lsp-diagnostics-flymake-default-message-formatter' reproduces the
existing behaviour: append `[code]' when the diagnostic carries one, and
return the bare message otherwise.

Unlike flycheck — which holds the message, id, severity, and checker on
separate slots of `flycheck-error' and lets each consumer (the error
list, `sideline-flycheck', etc.) compose them at display time — flymake
exposes only `flymake-diagnostic-text'.  Any presentation choice
therefore has to be made before the diagnostic is constructed.  The new
hook provides that single point of customisation, letting users suppress
the code, prepend the source, add a severity prefix, or reformat the
message in any other way without overriding
`lsp-diagnostics--flymake-update-diagnostics'.
@alberti42 alberti42 force-pushed the show-diagnostic-codes branch from 723a064 to 0057ce3 Compare May 14, 2026 09:48
@alberti42
Copy link
Copy Markdown
Contributor Author

Dear Jen-Chieh (@jcs090218),

Profiting from some public holiday in Germany, I went back to this PR. I'll split my reply into two GitHub comments to keep concerns separate. The first message, provided directly here below, closes the loop on the original PR; the other comment addresses the composable-formatter follow-up you asked for.

Yeah, there are breaking changes on the Emacs snapshot. No need to
worry about those job failures (at least for now). :)

Thanks for confirming the CI snapshot failures are unrelated.

About the rendering inconsistency you spotted: when I said flycheck
already showed the code, I was testing with lsp-ui-sideline as my
flycheck frontend. Your screenshot was taken with
sideline-flycheck, which I did not know yet.

I found out that the problem was in the other package sideline-flycheck,
which was using flycheck-error-message to render the message instead
of flycheck-error-format-message-and-id, thus effectively dropping
the ID.

To fix it, I have opened a small companion PR to sideline-flycheck:

  • emacs-sideline/sideline-flycheck#17: adds an opt-in
    sideline-flycheck-show-error-id defcustom (default nil) that
    switches the render path to flycheck-error-format-message-and-id.

With that PR and this one, the two frontends are aligned: any user can
have the diagnostic code visible regardless of whether they use flymake
or flycheck (via flycheck-sideline or lsp-ui-sideline).

The first commit on this branch (19d5f31feat: append LSP
diagnostic codes to flymake messages
) delivers exactly what the
original PR description promised, and when the sideline-flycheck
companion PR lands, the original inconsistency will be fully resolved.

By the way — having now spent some time inside sideline-flycheck and
sideline-lsp, I really like the design and have switched my own
config to use them instead of lsp-ui. Thanks for that work too.

@alberti42
Copy link
Copy Markdown
Contributor Author

I prefer the Composable formatter approach because it's more
flexible and gives users greater control.

I went ahead and implemented that. It's on the same branch as a
separate, follow-up commit:

  • 0057ce3: feat: composable flymake message formatter for LSP
    diagnostics

I am happy to split this into its own PR if you'd rather review and land
the two changes independently; the first commit stands on its own
and fulfills what the original PR description promised.

What was added in the second commit

A defcustom lsp-diagnostics-flymake-message-formatter whose default
(lsp-diagnostics-flymake-default-message-formatter) reproduces the
existing behavior: append [code] when present, bare message
otherwise. Flymake users wanting a different rendering (e.g., bare message, prepend
source, severity prefix, etc.) can swap in their own function. Here below are two
examples of how the user may want to customize the message:

;; Suppress the bracketed code:
(setq lsp-diagnostics-flymake-message-formatter
      (lambda (diag) (lsp-get diag :message)))

;; Prepend the source (e.g. "basedpyright: foo is unused [reportXxx]"):
(setq lsp-diagnostics-flymake-message-formatter
      (lambda (diag)
        (let ((src (lsp-get diag :source?))
              (msg (lsp-get diag :message))
              (code (lsp-get diag :code?)))
          (concat (and src (format "%s: " src))
                  msg
                  (and code (format " [%s]" code))))))

Why a hook makes sense on flymake but not on flycheck

The two diagnostics frontends have very different architectures, and
that asymmetry is what makes the composable hook genuinely useful
for flymake:

flycheck stores structured fields on flycheck-error:
:message, :id, :severity, :checker, etc. lsp-mode hands those
fields over already separated (see lsp-diagnostics--flycheck-start,
which sets :id code? and :group source?) and walks away. Each
consumer composes its own display:

  • flycheck-list-errors renders the ID in its own column.
  • sideline-flycheck calls flycheck-error-message by default (hence
    the dropped code in your screenshot) and will call
    flycheck-error-format-message-and-id once the companion PR lands.
  • Tooltip / minibuffer formatters do their own thing.

So a flycheck user wanting a different rendering already has a knob,
i.e., they can use/write their renderer. There is no flycheck-side composition
happening in lsp-mode that a user could override.

flymake, by contrast, exposes a single flymake-diagnostic-text
slot per diagnostic. Whatever string is in that slot is what flymake
displays everywhere — margins, echo area,
flymake-show-buffer-diagnostics. There is no downstream knob to
turn, because flymake has no concept of "the id is a separate field,
render it however you like."

That means flymake is the only place where lsp-mode is forced to
pre-compose the displayable string. And that's the only place a
composable hook in lsp-mode would genuinely add something
that cannot otherwise be achieved without resorting to hacky "advices".

Test plan (verified on my local fork)

  • Default formatter: flymake margin and flymake-show-buffer-diagnostics
    show message [code]; diagnostics without a code (parser-level
    errors) render unchanged with no trailing brackets.
  • Override with (lambda (diag) (lsp-get diag :message)): codes
    disappear; bare messages remain bare.
  • Restore the default: codes come back.

Verified end to end using basedpyright on a Python buffer.

@jcs090218
Copy link
Copy Markdown
Member

I found out that the problem was in the other package sideline-flycheck,
which was using flycheck-error-message to render the message instead
of flycheck-error-format-message-and-id, thus effectively dropping
the ID.

I'm sorry I didn't brought up this in the first place! 😓

By the way — having now spent some time inside sideline-flycheck and
sideline-lsp, I really like the design and have switched my own
config to use them instead of lsp-ui. Thanks for that work too.

I'm glad you like it! :D

Copy link
Copy Markdown
Member

@jcs090218 jcs090218 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me.

Can you update the changelog as well? Thank you! :D

Comment thread lsp-diagnostics.el Outdated
reformat the message in any other way."
:type 'function
:group 'lsp-diagnostics
:package-version '(lsp-mode . "9.1.0"))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version number is now 10.0.1. :)

One entry per commit:
- the composable `lsp-diagnostics-flymake-message-formatter' defcustom,
- the default-on append of the LSP diagnostic code to flymake messages.
@alberti42
Copy link
Copy Markdown
Contributor Author

Can you update the changelog as well? Thank you! :D

Awesome. I just updated the CHANGELOG.org

PS: I realized that I never updated the CHANGELOG.org for the other 4 PRs that already landed upstream. There is also one more PR #5057 of mine under review. If you'd like, I could could post changelog suggestions in the comments of the respective PR pages so that you can push those suggestions upstream, or simply as a single block for easy copy&paste for you. These other PRs were about under-the-hood bug fixes. But I'm not sure if you'd want them in the CHANGELOG visible to the users...

@jcs090218
Copy link
Copy Markdown
Member

PS: I realized that I never updated the CHANGELOG.org for the other 4 PRs that already landed upstream. There is also one more PR #5057 of mine under review. If you'd like, I could could post changelog suggestions in the comments of the respective PR pages so that you can push those suggestions upstream, or simply as a single block for easy copy&paste for you. These other PRs were about under-the-hood bug fixes. But I'm not sure if you'd want them in the CHANGELOG visible to the users...

No worries about those PRs. No, every PR needs to update the CHANGELOG. Normally, we don't add a changelog entry for bug fixes unless it's critical and worth mentioning. 🤔

@jcs090218 jcs090218 merged commit 2489b04 into emacs-lsp:master May 14, 2026
11 of 14 checks passed
@jcs090218
Copy link
Copy Markdown
Member

Merged! Thank you!

@alberti42
Copy link
Copy Markdown
Contributor Author

Thanks for the review work and always the prompt replies!

@alberti42 alberti42 deleted the show-diagnostic-codes branch May 15, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants